/*
 * @(#)RedirectionModule.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.io.IOException;
import java.net.ProtocolException;
import java.util.Hashtable;

/**
 * This module handles the redirection status codes 301, 302, 303, 305, 306 and
 * 307.
 *
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 */
class RedirectionModule implements HTTPClientModule
{
   /** a list of permanent redirections (301) */
   private static Hashtable perm_redir_cntxt_list = new Hashtable();

   /** a list of deferred redirections (used with Response.retryRequest()) */
   private static Hashtable deferred_redir_list = new Hashtable();

   /** the level of redirection */
   private int level;

   /** the url used in the last redirection */
   private URI lastURI;

   /** used for deferred redirection retries */
   private boolean new_con;

   /** used for deferred redirection retries */
   private Request saved_req;

   private static final Logger log = Logger.getLogger(RedirectionModule.class);

   // Constructors

   /**
    * Start with level 0.
    */
   RedirectionModule()
   {
      level = 0;
      lastURI = null;
      saved_req = null;
   }

   // Methods

   /**
    * Invoked by the HTTPClient.
    */
   public int requestHandler(Request req, Response[] resp)
   {
      HTTPConnection con = req.getConnection();
      URI new_loc, cur_loc;

      // check for retries

      HttpOutputStream out = req.getStream();
      if (out != null && deferred_redir_list.get(out) != null)
      {
         copyFrom((RedirectionModule)deferred_redir_list.remove(out));
         req.copyFrom(saved_req);

         if (new_con)
            return REQ_NEWCON_RST;
         else
            return REQ_RESTART;
      }

      // handle permanent redirections

      try
      {
         cur_loc = new URI(new URI(con.getProtocol(), con.getHost(), con.getPort(), null), req.getRequestURI());
      }
      catch (ParseException pe)
      {
         throw new Error("HTTPClient Internal Error: unexpected exception '" + pe + "'");
      }

      // handle permanent redirections

      Hashtable perm_redir_list = Util.getList(perm_redir_cntxt_list, req.getConnection().getContext());
      if ((new_loc = (URI)perm_redir_list.get(cur_loc)) != null)
      {
         /*
          * copy query if present in old url but not in new url. This isn't
          * strictly conforming, but some scripts fail to properly propagate the
          * query string to the Location header. Unfortunately it looks like we're
          * fucked either way: some scripts fail if you don't propagate the query
          * string, some fail if you do... God, don't you just love it when people
          * can't read a spec? Anway, since we can't get it right for all scripts
          * we opt to follow the spec. String nres = new_loc.getPathAndQuery(),
          * oquery = Util.getQuery(req.getRequestURI()), nquery =
          * Util.getQuery(nres); if (nquery == null && oquery != null) nres += "?"
          * + oquery;
          */
         String nres = new_loc.getPathAndQuery();
         req.setRequestURI(nres);

         try
         {
            lastURI = new URI(new_loc, nres);
         }
         catch (ParseException pe)
         {
         }

         if (log.isDebugEnabled())
            log.debug("Matched request in permanent redirection list - redoing request to " + lastURI.toExternalForm());

         if (!con.isCompatibleWith(new_loc))
         {
            try
            {
               con = new HTTPConnection(new_loc);
            }
            catch (Exception e)
            {
               throw new Error("HTTPClient Internal Error: unexpected " + "exception '" + e + "'");
            }

            con.setContext(req.getConnection().getContext());
            req.setConnection(con);
            return REQ_NEWCON_RST;
         }
         else
         {
            return REQ_RESTART;
         }
      }

      return REQ_CONTINUE;
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void responsePhase1Handler(Response resp, RoRequest req) throws IOException
   {
      int sts = resp.getStatusCode();
      if (sts < 301 || sts > 307 || sts == 304)
      {
         if (lastURI != null) // it's been redirected
            resp.setEffectiveURI(lastURI);
      }
   }

   /**
    * Invoked by the HTTPClient.
    */
   public int responsePhase2Handler(Response resp, Request req) throws IOException
   {
      /* handle various response status codes until satisfied */

      int sts = resp.getStatusCode();
      switch (sts)
      {
         case 302 : // General (temporary) Redirection (handle like 303)

            /*
             * Note we only do this munging for POST and PUT. For GET it's not
             * necessary; for HEAD we probably want to do another HEAD. For all
             * others (i.e. methods from WebDAV, IPP, etc) it's somewhat unclear -
             * servers supporting those should really return a 307 or 303, but some
             * don't (guess who...), so we just don't touch those.
             */
            if (req.getMethod().equals("POST") || req.getMethod().equals("PUT"))
            {
               if (log.isDebugEnabled())
                  log.debug("Received status: " + sts + " " + resp.getReasonLine() + " - treating as 303");

               sts = 303;
            }

         case 301 : // Moved Permanently
         case 303 : // See Other (use GET)
         case 307 : // Moved Temporarily (we mean it!)

            if (log.isDebugEnabled())
               log.debug("Handling status: " + sts + " " + resp.getReasonLine());

            // the spec says automatic redirection may only be done if
            // the second request is a HEAD or GET.
            if (!req.getMethod().equals("GET") && !req.getMethod().equals("HEAD") && sts != 303)
            {
               if (log.isDebugEnabled())
                  log.debug("Not redirected because method is neither HEAD nor GET");

               if (sts == 301 && resp.getHeader("Location") != null)
                  update_perm_redir_list(req, resLocHdr(resp.getHeader("Location"), req));

               resp.setEffectiveURI(lastURI);
               return RSP_CONTINUE;
            }

         case 305 : // Use Proxy
         case 306 : // Switch Proxy

            if (sts == 305 || sts == 306)
            {
               if (log.isDebugEnabled())
                  log.debug("Handling status: " + sts + " " + resp.getReasonLine());

            }

            // Don't accept 305 from a proxy
            if (sts == 305 && req.getConnection().getProxyHost() != null)
            {
               if (log.isDebugEnabled())
                  log.debug("305 ignored because a proxy is already in use");

               resp.setEffectiveURI(lastURI);
               return RSP_CONTINUE;
            }

            /*
             * the level is a primitive way of preventing infinite redirections.
             * RFC-2068 set the max to 5, but RFC-2616 has loosened this. Since some
             * sites (notably M$) need more levels, this is now set to the
             * (arbitrary) value of 15 (god only knows why they need to do even 5
             * redirections...).
             */
            if (level >= 15 || resp.getHeader("Location") == null)
            {
               if (log.isDebugEnabled())
               {
                  if (level >= 15)
                     log.debug("Not redirected because of too many levels of redirection");
                  else
                     log.debug("Not redirected because no Location header was present");
               }

               resp.setEffectiveURI(lastURI);
               return RSP_CONTINUE;
            }
            level++;

            URI loc = resLocHdr(resp.getHeader("Location"), req);

            HTTPConnection mvd;
            String nres;
            new_con = false;

            if (sts == 305)
            {
               mvd =
                  new HTTPConnection(req.getConnection().getProtocol(), req.getConnection().getHost(), req
                     .getConnection().getPort());
               mvd.setCurrentProxy(loc.getHost(), loc.getPort());
               mvd.setContext(req.getConnection().getContext());
               new_con = true;

               nres = req.getRequestURI();

               /*
                * There was some discussion about this, and especially Foteos
                * Macrides (Lynx) said a 305 should also imply a change to GET (for
                * security reasons) - see the thread starting at
                * http://www.ics.uci.edu/pub/ietf/http/hypermail/1997q4/0351.html
                * However, this is not in the latest draft, but since I agree with
                * Foteos we do it anyway...
                */
               req.setMethod("GET");
               req.setData(null);
               req.setStream(null);
            }
            else if (sts == 306)
            {
               // We'll have to wait for Josh to create a new spec here.
               return RSP_CONTINUE;
            }
            else
            {
               if (req.getConnection().isCompatibleWith(loc))
               {
                  mvd = req.getConnection();
                  nres = loc.getPathAndQuery();
               }
               else
               {
                  try
                  {
                     mvd = new HTTPConnection(loc);
                     nres = loc.getPathAndQuery();
                  }
                  catch (Exception e)
                  {
                     if (req.getConnection().getProxyHost() == null || !loc.getScheme().equalsIgnoreCase("ftp"))
                        return RSP_CONTINUE;

                     // We're using a proxy and the protocol is ftp -
                     // maybe the proxy will also proxy ftp...
                     mvd =
                        new HTTPConnection("http", req.getConnection().getProxyHost(), req.getConnection()
                           .getProxyPort());
                     mvd.setCurrentProxy(null, 0);
                     nres = loc.toExternalForm();
                  }

                  mvd.setContext(req.getConnection().getContext());
                  new_con = true;
               }

               /*
                * copy query if present in old url but not in new url. This isn't
                * strictly conforming, but some scripts fail to propagate the query
                * properly to the Location header. See comment on line 126. String
                * oquery = Util.getQuery(req.getRequestURI()), nquery =
                * Util.getQuery(nres); if (nquery == null && oquery != null) nres +=
                * "?" + oquery;
                */

               if (sts == 303)
               {
                  // 303 means "use GET"

                  if (!req.getMethod().equals("HEAD"))
                     req.setMethod("GET");
                  req.setData(null);
                  req.setStream(null);
               }
               else
               {
                  // If they used an output stream then they'll have
                  // to do the resend themselves
                  if (req.getStream() != null)
                  {
                     if (!HTTPConnection.deferStreamed)
                     {
                        if (log.isDebugEnabled())
                           log.debug("Status " + sts + " not handled - request has an output stream");

                        return RSP_CONTINUE;
                     }

                     saved_req = (Request)req.clone();
                     deferred_redir_list.put(req.getStream(), this);
                     req.getStream().reset();
                     resp.setRetryRequest(true);
                  }

                  if (sts == 301)
                  {
                     // update permanent redirection list
                     try
                     {
                        update_perm_redir_list(req, new URI(loc, nres));
                     }
                     catch (ParseException pe)
                     {
                        throw new Error("HTTPClient Internal Error: " + "unexpected exception '" + pe + "'");
                     }
                  }
               }

               // Adjust Referer, if present
               NVPair[] hdrs = req.getHeaders();
               for (int idx = 0; idx < hdrs.length; idx++)
                  if (hdrs[idx].getName().equalsIgnoreCase("Referer"))
                  {
                     HTTPConnection con = req.getConnection();
                     hdrs[idx] = new NVPair("Referer", con + req.getRequestURI());
                     break;
                  }
            }

            req.setConnection(mvd);
            req.setRequestURI(nres);

            try
            {
               resp.getInputStream().close();
            }
            catch (IOException ioe)
            {
            }

            if (sts != 305 && sts != 306)
            {
               try
               {
                  lastURI = new URI(loc, nres);
               }
               catch (ParseException pe)
               { /* ??? */
               }

               if (log.isDebugEnabled())
                  log.debug("Request redirected to " + lastURI.toExternalForm() + " using method " + req.getMethod());
            }
            else
            {
               if (log.isDebugEnabled())
                  log.debug("Resending request using " + "proxy " + mvd.getProxyHost() + ":" + mvd.getProxyPort());
            }

            if (req.getStream() != null)
               return RSP_CONTINUE;
            else if (new_con)
               return RSP_NEWCON_REQ;
            else
               return RSP_REQUEST;

         default :

            return RSP_CONTINUE;
      }
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void responsePhase3Handler(Response resp, RoRequest req)
   {
   }

   /**
    * Invoked by the HTTPClient.
    */
   public void trailerHandler(Response resp, RoRequest req)
   {
   }

   /**
    * Update the permanent redirection list.
    *
    * @param the original request
    * @param the new location
    */
   private static void update_perm_redir_list(RoRequest req, URI new_loc)
   {
      HTTPConnection con = req.getConnection();
      URI cur_loc = null;
      try
      {
         cur_loc = new URI(new URI(con.getProtocol(), con.getHost(), con.getPort(), null), req.getRequestURI());
      }
      catch (ParseException pe)
      {
      }

      if (!cur_loc.equals(new_loc))
      {
         Hashtable perm_redir_list = Util.getList(perm_redir_cntxt_list, con.getContext());
         perm_redir_list.put(cur_loc, new_loc);
      }
   }

   /**
    * The Location header field must be an absolute URI, but too many broken
    * servers use relative URIs. So, we always resolve relative to the full
    * request URI.
    *
    * @param loc the Location header field
    * @param req the Request to resolve relative URI's relative to
    * @return an absolute URI corresponding to the Location header field
    * @exception ProtocolException if the Location header field is completely
    *            unparseable
    */
   private URI resLocHdr(String loc, RoRequest req) throws ProtocolException
   {
      try
      {
         URI base =
            new URI(req.getConnection().getProtocol(), req.getConnection().getHost(), req.getConnection().getPort(),
               null);
         base = new URI(base, req.getRequestURI());
         URI res = new URI(base, loc);
         if (res.getHost() == null)
            throw new ProtocolException("Malformed URL in Location header: `" + loc + "' - missing host field");
         return res;
      }
      catch (ParseException pe)
      {
         throw new ProtocolException("Malformed URL in Location header: `" + loc + "' - exception was: "
            + pe.getMessage());
      }
   }

   private void copyFrom(RedirectionModule other)
   {
      this.level = other.level;
      this.lastURI = other.lastURI;
      this.saved_req = other.saved_req;
   }
}
